Skip to content

fix(orchestrate): never put a second agent on one worktree (re-entry) - #26

Merged
choiyounggi merged 1 commit into
mainfrom
t2-reentry-idempotency
Aug 5, 2026
Merged

fix(orchestrate): never put a second agent on one worktree (re-entry)#26
choiyounggi merged 1 commit into
mainfrom
t2-reentry-idempotency

Conversation

@choiyounggi

Copy link
Copy Markdown
Owner

The gap

SKILL.md's "Re-entry (resume)" restarts a proven-dead worker with --worktree id:<…> --agent claude. GROUNDWORK_ESCALATION_DIR is always set for a dev-loop worker, so that lands in WORKER MODE, whose body ran orca terminal create unconditionally.

If the coordinator's liveness read was wrong — or the worker was only briefly unreachable — that put a second Claude agent on the same checkout, with the Dispatch bound to only one of them. Two agents editing one working tree corrupt each other's edits silently. The tmux path has no such hazard: launch-session.sh detects the existing session and reuses it, loudly, on stderr.

The fix

Worker mode is now idempotent on re-entry, automatically — no flag to pass. Before creating anything it asks Orca whether this task's Dispatch already names a terminal that is still live on the target worktree:

  • orchestration dispatch-show --task <id>.result.dispatch.assignee_handle
  • joined against terminal list --worktree <selector>, requiring orphaned != true and connected == true
Probe result Behavior
live binds that terminal, creates nothing, says so on stderr (--perm/--name are inert)
not live unchanged: terminal create + tui-idle wait + bind
no record unchanged (the task's first start)
can't tell exit 6, creates nothing, not retried automatically

The reuse target is identified by the Dispatch record rather than a title/preview heuristic because a single worktree can hold both an agent terminal and a plain shell — binding a Dispatch to the user's shell would type the worker preamble into bash.

Why "unknown" refuses instead of creating

The two error directions are asymmetric. A wrong create = two agents on one checkout: silent and unrecoverable by the time anyone notices. A wrong refusal = one restart stalls, loudly and exit-coded, and the coordinator can orca orchestration worker-stop and re-run. This mirrors orca-worktree-alive.sh's existing rule that "unknown" is never a definite answer that causes damage.

Known limitation (stated, not hidden)

This is a check-then-create, so it protects a sequential re-entry, not two coordinators racing on one worktree. Closing that window needs resource-side exclusivity (Orca refusing a second agent terminal per worktree, or a fencing token on the Dispatch).

Tests

tests/orca-worker-start.bats: 29 → 42 tests (13 new — 2 normal, 5 error, 6 boundary). All 29 pre-existing tests are byte-identical (0 removed lines); every pre-existing fail-closed guard still holds.

  • npx bats tests/147 passing, 0 failures
  • sh -n / dash -n clean, no bashisms (POSIX sh, set -u preserved)
  • Both new behaviors mutation-checked: [ "$live" -ge 1 ]-ge 0 turns the 4 boundary tests red; making the refusal guards unreachable turns all 5 exit-6 tests red
  • Independent test-quality-auditor audit: VERDICT: PASS (re-ran both mutants itself)

Also included: a test-isolation fix

setup() now does unset GROUNDWORK_ESCALATION_DIR GROUNDWORK_TASK_ID. A dev-loop worker session exports that variable and bats' run env FOO=bar … inherits the caller's environment, so 6 pre-existing tests failed at baseline when the suite was run inside a worker session (verified by stashing the test file and re-running). CI never saw it because CI's environment is clean. This is an addition to setup() only — no existing test body was touched.

Follow-ups (deliberately out of scope)

  • </dev/null on the three pre-existing orca calls in this file (the new probe calls have it)
  • The same re-entry probe for orca-spawn.sh, which has the same unconditional-create shape

🤖 Generated with Claude Code

SKILL.md's re-entry path restarts a worker the coordinator believes is dead
by calling orca-worker-start.sh again with --worktree/--agent. For a dev-loop
worker GROUNDWORK_ESCALATION_DIR is always set, so that lands in worker mode,
which ran `orca terminal create` unconditionally. If the liveness read was
wrong — and this run proved a worker can be ALIVE yet wedged — that puts a
second agent on the same checkout with the Dispatch bound to only one of
them. The tmux path has no such hazard: launch-session.sh detects an existing
session and reuses it, loudly.

Worker mode now probes first, automatically, with no flag to pass: it joins
`orchestration dispatch-show --task` against `terminal list --worktree` and
binds the recorded assignee when that terminal is still live on this
worktree, reporting the reuse on stderr.

Fails toward the safe side. Creating the second agent is the damaging
direction, so a probe that cannot answer is UNKNOWN, never "no agent": an
unreadable dispatch or terminal list exits 6 and creates nothing. Only
`ok:true` with `dispatch:null` counts as a definite first start. Both probe
calls take `</dev/null` so a headless coordinator cannot block on a prompt.
Reuse also skips the tui-idle wait — that exists to pass a new CLI's trust
screen, and a mid-task agent may never report idle.

Field names verified against the live runtime, not assumed: `terminal list`
exposes `orphaned` and `connected`; `dispatch-show` exposes `assignee_handle`.

Every pre-existing fail-closed guard is untouched. Tests 134 -> 147: reuse,
first start, orphaned, disconnected, empty list, assignee on another
worktree, both probes returning ok:false, both returning malformed JSON, a
blank payload, and proof the probe never runs in --terminal or composed mode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@choiyounggi
choiyounggi merged commit 5bd41a1 into main Aug 5, 2026
2 checks passed
@choiyounggi
choiyounggi deleted the t2-reentry-idempotency branch August 5, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant